﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace Ewk.MachineLearning.Genetic.Programming.Program.Functions
{
    internal class FunctionSet
    {
        private static readonly Random Random = new Random(DateTime.UtcNow.Millisecond);

        private readonly FunctionFactory _functionFactory = new FunctionFactory();
        private readonly List<Function> _functions;

        #region Constructors

        public FunctionSet()
        {
            _functions = InitializeDefaultFunctions().ToList();
        }

        public FunctionSet(IEnumerable<Function> functions)
        {
            _functions = functions.ToList();
        }

        #endregion

        /// <summary>
        /// Gets a random function from the function set.
        /// </summary>
        /// <param name="expectedOutputTypes"> </param>
        public Function GetRandom(IEnumerable<Type> expectedOutputTypes)
        {
            var suitableFunctions = _functions
                .Where(function =>
                       function.OutputDefinition.Types
                           .Where(expectedOutputTypes.Contains)
                           .Count() != 0)
                .ToList();

            var i = Random.Next(suitableFunctions.Count);

            return suitableFunctions.ElementAt(i);
        }

        /// <summary>
        /// Gets a random function from the function set with the specified arity.
        /// </summary>
        public Function GetRandom(int arity)
        {
            var candidates = _functions
                .Where(function =>
                       function.Arity == arity)
                .ToList();

            var i = Random.Next(candidates.Count());

            return candidates.ElementAt(i);
        }

        private IEnumerable<Function> InitializeDefaultFunctions()
        {
            return new List<Function>
                       {
                           // _functionFactory.CreateNumericFunction(objects => objects.Average(o => o), int.MaxValue), // Should have a List a input
                           _functionFactory.CreateNumericToNumericFunction(objects => Math.Abs(objects.Single()), 1, "Abs"),
                           _functionFactory.CreateNumericToNumericFunction(objects => Math.Cos(objects.Single()), 1, "Cos"),
                           _functionFactory.CreateNumericToNumericFunction(objects => Math.Cosh(objects.Single()), 1, "Cosh"),
                           _functionFactory.CreateNumericToNumericFunction(objects => Math.Acos(objects.Single()), 1, "Acos"),
                           _functionFactory.CreateNumericToNumericFunction(objects => Math.Sin(objects.Single()), 1, "Sin"),
                           _functionFactory.CreateNumericToNumericFunction(objects => Math.Sinh(objects.Single()), 1, "Sinh"),
                           _functionFactory.CreateNumericToNumericFunction(objects => Math.Asin(objects.Single()), 1, "Asin"),
                           _functionFactory.CreateNumericToNumericFunction(objects => Math.Tan(objects.Single()), 1, "Tan"),
                           _functionFactory.CreateNumericToNumericFunction(objects => Math.Atan(objects.Single()), 1, "Atan"),
                           _functionFactory.CreateNumericToNumericFunction(objects => Math.Log(objects.Single()), 1, "Log"),
                           _functionFactory.CreateNumericToNumericFunction(objects => Math.Log10(objects.Single()), 1, "Log10"),
                           _functionFactory.CreateNumericToNumericFunction(objects => Math.Exp(objects.Single()), 1, "Exp"),
                           _functionFactory.CreateNumericToNumericFunction(objects => Math.Sign(objects.Single()), 1, "Sign"),
                           _functionFactory.CreateNumericToNumericFunction(objects => Math.Sqrt(objects.Single()), 1, "Sqrt"),
                           _functionFactory.CreateNumericToStringFunction(objects => objects.Single().ToString(), 1, "ToString"),
                           _functionFactory.CreateNumericToNumericFunction(objects => objects.Aggregate((o1, o2) => o1 + o2), 2, "+"), // Aggregates can also have a list as input
                           _functionFactory.CreateNumericToNumericFunction(objects => objects.Aggregate((o1, o2) => o1 - o2), 2, "-"),
                           _functionFactory.CreateNumericToNumericFunction(objects => objects.Aggregate((o1, o2) => o1*o2), 2, "*"),
                           _functionFactory.CreateNumericToNumericFunction(objects => objects.Aggregate((o1, o2) => o2 == 0 ? 0 : o1/o2), 2, "/"),
                           _functionFactory.CreateNumericToNumericFunction(objects => objects.Aggregate((o1, o2) => o2 == 0 ? 0 : o1%o2), 2, "%"),
                           _functionFactory.CreateNumericToNumericFunction(objects => objects.Max(), 2, "Max"), // Can also have a List a input
                           _functionFactory.CreateNumericToNumericFunction(objects => objects.Min(), 2, "Min"), // Can also have a List a input
                           _functionFactory.CreateNumericToNumericFunction(objects => Math.Pow(objects.ElementAt(0), objects.ElementAt(1)), 2, "Pow"),

                           // Loops: do, while, for, foreach
/*
                           _functionFactory.CreateNumericFunction(objects =>
                                                                      {
                                                                          var result = objects.ElementAt(0);
                                                                          while (result < objects.ElementAt(1))
                                                                          {
                                                                              result += objects.ElementAt(0);
                                                                          }
                                                                          return result;
                                                                      }, 2),
                           _functionFactory.CreateNumericFunction(objects =>
                                                                      {
                                                                          var result = objects.ElementAt(0);
                                                                          while (result <= objects.ElementAt(1))
                                                                          {
                                                                              result += objects.ElementAt(0);
                                                                          }
                                                                          return result;
                                                                      }, 2),
                           _functionFactory.CreateNumericFunction(objects =>
                                                                      {
                                                                          var result = objects.ElementAt(0);
                                                                          do
                                                                          {
                                                                              result += objects.ElementAt(0);
                                                                          } while (result < objects.ElementAt(1));
                                                                          return result;
                                                                      }, 2),
                           _functionFactory.CreateNumericFunction(objects =>
                                                                      {
                                                                          var result = objects.ElementAt(0);
                                                                          do
                                                                          {
                                                                              result += objects.ElementAt(0);
                                                                          } while (result <= objects.ElementAt(1));
                                                                          return result;
                                                                      }, 2),

                            _functionFactory.CreateNumericFunction(objects =>
                                                                      {
                                                                          var result = objects.ElementAt(0);
                                                                          while (result < objects.ElementAt(1))
                                                                          {
                                                                              result *= objects.ElementAt(0);
                                                                          }
                                                                          return result;
                                                                      }, 2),
                           _functionFactory.CreateNumericFunction(objects =>
                                                                      {
                                                                          var result = objects.ElementAt(0);
                                                                          while (result <= objects.ElementAt(1))
                                                                          {
                                                                              result *= objects.ElementAt(0);
                                                                          }
                                                                          return result;
                                                                      }, 2),
                           _functionFactory.CreateNumericFunction(objects =>
                                                                      {
                                                                          var result = objects.ElementAt(0);
                                                                          do
                                                                          {
                                                                              result *= objects.ElementAt(0);
                                                                          } while (result < objects.ElementAt(1));
                                                                          return result;
                                                                      }, 2),
                           _functionFactory.CreateNumericFunction(objects =>
                                                                      {
                                                                          var result = objects.ElementAt(0);
                                                                          do
                                                                          {
                                                                              result *= objects.ElementAt(0);
                                                                          } while (result <= objects.ElementAt(1));
                                                                          return result;
                                                                      }, 2),

                            _functionFactory.CreateNumericFunction(objects =>
                                                                      {
                                                                          var result = objects.ElementAt(0);
                                                                          while (result > objects.ElementAt(1))
                                                                          {
                                                                              result -= objects.ElementAt(0);
                                                                          }
                                                                          return result;
                                                                      }, 2),
                           _functionFactory.CreateNumericFunction(objects =>
                                                                      {
                                                                          var result = objects.ElementAt(0);
                                                                          while (result >= objects.ElementAt(1))
                                                                          {
                                                                              result -= objects.ElementAt(0);
                                                                          }
                                                                          return result;
                                                                      }, 2),
                           _functionFactory.CreateNumericFunction(objects =>
                                                                      {
                                                                          var result = objects.ElementAt(0);
                                                                          do
                                                                          {
                                                                              result -= objects.ElementAt(0);
                                                                          } while (result > objects.ElementAt(1));
                                                                          return result;
                                                                      }, 2),
                           _functionFactory.CreateNumericFunction(objects =>
                                                                      {
                                                                          var result = objects.ElementAt(0);
                                                                          do
                                                                          {
                                                                              result -= objects.ElementAt(0);
                                                                          } while (result >= objects.ElementAt(1));
                                                                          return result;
                                                                      }, 2),
*/

                           _functionFactory.CreateNumericToNumericFunction(objects => objects.ElementAt(0) < objects.ElementAt(1)
                                                                                 ? objects.ElementAt(2)
                                                                                 : objects.ElementAt(3), 4, "IF 0<1?3:4"),
                           _functionFactory.CreateNumericToNumericFunction(
                               objects => objects.ElementAt(0) <= objects.ElementAt(1)
                                              ? objects.ElementAt(2)
                                              : objects.ElementAt(3), 4, "IF 0<=1?3:4"),
                           _functionFactory.CreateNumericToNumericFunction(
                               objects => objects.ElementAt(0) == objects.ElementAt(1)
                                              ? objects.ElementAt(2)
                                              : objects.ElementAt(3), 4, "IF 0==1?3:4"),
                           _functionFactory.CreateNumericToNumericFunction(
                               objects => objects.ElementAt(0) >= objects.ElementAt(1)
                                              ? objects.ElementAt(2)
                                              : objects.ElementAt(3), 4, "IF 0>=1?3:4"),
                           _functionFactory.CreateNumericToNumericFunction(objects => objects.ElementAt(0) > objects.ElementAt(1)
                                                                                 ? objects.ElementAt(2)
                                                                                 : objects.ElementAt(3), 4, "IF 0>1?3:4"),
                       };
        }
    }
}